package data;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.swing.JOptionPane;

import miscellaneous.Storage;

import client.Synchronizer;
import client.messages.InviteOverview;
import client.popups.appointment.AppointmentOverview;
import client.popups.meeting.MeetingOverview;

/**
 * An ArrayList that automatically notifies listeners when changes happen. 
 * It also contacts the server and retrieves objects as necessary.
 * @author Erik
 *
 */
public class SynchronizedList<E extends Data> implements PropertyChangeListener, Iterable<E> {
	private List<E> objects;
	private Map<Integer,E> map;
	private Synchronizer<E> synchronizer;
	
	private List<E> toBeCreated;
	private List<E> toBeUpdated;
	private List<E> toBeDeleted;
	
	private PropertyChangeSupport changeSupport;

	/**
	 * Creates a list containing the specified class 
	 * @param listClass The class to be contained (the same as E)
	 */
	public SynchronizedList(Class<E> listClass) {

		objects = new ArrayList<E>();
		map = new HashMap<Integer,E>();
		toBeCreated = new ArrayList<E>();
		toBeUpdated = new ArrayList<E>();
		toBeDeleted = new ArrayList<E>();
		
		changeSupport = new PropertyChangeSupport(this);
		synchronizer = new Synchronizer<E>(listClass);
	}

	/**
	 * Returns the object at the specified index in the list
	 * @param index The index of the object
	 * @return The object at the index
	 */
	public E get(int index) {
		return objects.get(index);
	}
	
	/**
	 * Returns the object with the specified id.
	 * If the object is not found in the list, the server is
	 * queried. If neither the list or the server has this object,
	 * null is returned.
	 * @param id The id of the object to be returned.
	 * @return The object who's id is queried
	 */
	public E getById(int id) {
		//try to find object in list
		if(map.containsKey(id))
			return map.get(id);

		//try to get object from server;
		E object = synchronizer.get(id);
		if(object != null) {
			synchronized(objects) {
				objects.add(object);
			}
			sort();
			synchronized(map) {
				map.put(id, object);
			}
		}
		
		return object;
	}
	/**
	 * Returns the index of an object
	 * @param object The object to get the index of
	 * @return The index of the object
	 */
	public int indexOf(E object) {
		return objects.indexOf(object);
	}
	
	/**
	 * Inserts the given object into the list, without creating it on the server.
	 * Should only be used if the object came from the server.
	 * @param object The object to be inserted
	 */
	public void insert(E object) {
		if(map.containsKey(object.getId()))
			return;
	
		synchronized(objects) {
			objects.add(object);
		}
		synchronized(map) {
			map.put(object.getId(), object);	
		}
		
		if(object instanceof Invite && ((Invite)object).getReceiver() == Storage.getloggedInUser() && 
				((Invite)object).getAnswer() == Invite.ANSWER_UNKOWN){
			InviteOverview.showOverview();
		}
		
		object.addPropertyChangeListener(this);
		changeSupport.firePropertyChange("addObject", null, object);
		
		Storage.updateUI();
	}
	
	/**
	 * Inserts all the objects in the list by repeatedly calling insert(E)
	 * @param objects
	 */
	public void insertAll(List<E> objects) {
		for (E o : objects) {
			insert(o);
		}
	}
	
	/**
	 * Add an object to this list. It also creates the object on the server.
	 * If the server refuses the object, it is <strong>not</strong> added!
	 * @param object The object to add
	 */
	public void add(E object) {
		synchronized(toBeCreated) {
			toBeCreated.add(object);
		}
		
		synchronize();
	}
	
	/**
	 * Removes the object based on the id
	 * @param id the objects id to remove
	 */
	public void deleteByID(int id) {
		E e = map.get(id);
		if(e != null) {
			objects.remove(e);
			map.remove(id);
			e.removePropertyChangeListener(this);
		}
	}

	/**
	 * Deletes the given object into the list, without deleting it on the server.
	 * Should only be used if the object came from the server.
	 * @param object The object to be deleteds
	 */
	public void delete(E object) {
		if(!map.containsKey(object.getId()))
			return;
		
		synchronized(objects) {
			objects.remove(object);
		}
		synchronized(map) {
			map.remove(object.getId());	
		}
		
		object.removePropertyChangeListener(this);
		changeSupport.firePropertyChange("removeObject", null, object);
		Storage.updateUI();
	}
	
	/**
	 * Remove an object from this list. It also deletes the object from the server.
	 * If the server refuses the deletion, the object is <strong>not</strong> removed!
	 * @param index The index of the event to remove
	 */
	public void remove(int index) {
		E object;
		synchronized(objects) {
			object = objects.get(index);
		}
		remove(object);
	}

	/**
	 * Remove an object from this list. It also deletes the object from the server.
	 * If the server refuses the deletion, the object is <strong>not</strong> removed!
	 * @param object The object to remove
	 */
	public void remove(E object) {	
		synchronized(toBeDeleted) {
			toBeDeleted.add(object);
		}
		
		synchronize();
	}
	
	/**
	 * Synchronizes the list to the server. Any remaining updates, creations and deletions are performed.
	 * If the action is successful or unsuccessful the action is evaluated. 
	 * If it fails, no further actions of the kind will be attempted.
	 * @return Whether everything was synchronized with the server
	 */
	public boolean synchronize() {
		
		// synchronize the created objects with the server
		synchronized(toBeCreated) {
			for(int index=0; index<toBeCreated.size(); index++) {
				E object = toBeCreated.get(index);
				int response = synchronizer.create(object);
				
				if(response == Synchronizer.FAILED) {
					System.err.println("SynchronizedList::synchronize: Failed to create object");
					break;
				} else if(response == Synchronizer.SUCCESSFUL) {
					synchronized (objects) {
						objects.add(object);
					}
					synchronized(map) {
						map.put(object.getId(), object);
					}
					object.addPropertyChangeListener(this);
					changeSupport.firePropertyChange("addObject", null, object);	
				} else if(response == Synchronizer.REFUSED) {
					System.err.println("SynchronizedList::synchronize: Server refused to create object");
					JOptionPane.showMessageDialog(null, "Serveren godtok ikke objektet", "Feil", JOptionPane.WARNING_MESSAGE);
				}
				toBeCreated.remove(index);
				index--;
			}	
		}
		
		// synchronize the updated objects with the server
		synchronized(toBeUpdated) {
			for(int index=0; index<toBeUpdated.size(); index++) {
				E object = toBeUpdated.get(index);
				int response = synchronizer.update(object);
				
				if(response == Synchronizer.FAILED) {
					System.err.println("SynchronizedList::synchronize: Failed to update object");
					break;
				} else if(response == Synchronizer.SUCCESSFUL) {
					synchronized (objects) {
						changeSupport.firePropertyChange("updateObject", null, object);	
					}
				} else if(response == Synchronizer.REFUSED) {
					System.err.println("SynchronizedList::synchronize: Server refused to update object");
					JOptionPane.showMessageDialog(null, "Serveren godtok ikke endringene", "Feil", JOptionPane.WARNING_MESSAGE);
					E correct = synchronizer.get(object.getId());
					object.removePropertyChangeListener(this);
					synchronized(objects) {
						objects.remove(object);
						objects.add(correct);
					}
					synchronized(map) {
						map.remove(object.getId());
						map.put(correct.getId(), correct);
					}
					changeSupport.firePropertyChange("update", null, null);
				}
				toBeUpdated.remove(index);
				index--;
			}	
		}
		
		// synchronize the created object with the server
		synchronized(toBeDeleted) {
			for(int index=0; index<toBeDeleted.size(); index++) {
				E object = toBeDeleted.get(index);
				int response = synchronizer.delete(object);
				
				if(response == Synchronizer.FAILED) {
					System.err.println("SynchronizedList::synchronize: Failed to delete object");
					break;
				} else if(response == Synchronizer.SUCCESSFUL) {
					synchronized (objects) {
						objects.remove(object);
					synchronized (map) {
						map.remove(object.getId());
					}
					
					if(object instanceof PropertyChangeSpeaker)
						((PropertyChangeSpeaker) object).removePropertyChangeListener(this);
					changeSupport.firePropertyChange("removeObject", null, object);
					
					}
				} else if(response == Synchronizer.REFUSED) {
					System.err.println("SynchronizedList::synchronize: Server refused to delete object");
					JOptionPane.showMessageDialog(null, "Serveren godtok ikke å slette", "Feil", JOptionPane.WARNING_MESSAGE);
				}
				toBeDeleted.remove(index);
				index--;
			}	
		}
		
		sort();
		Storage.updateUI();
		return queueSize() == 0;
	}
	
	/**
	 * Returns true if this list contains the object.
	 * @param object Object to check for
	 * @return Whether the object is in the list
	 */
	public boolean contains(E object) {
		return map.containsValue(object);
	}
	
	/**
	 * Returns whether the list contains an object with the given id.
	 * @param id The id to check for
	 * @return Whether an object with the id is found
	 */
	public boolean contains(int id) {
		return map.containsKey(id);
	}

	/**
	 * Returns the number of objects in the list
	 * @return The size of the list
	 */
	public int size() {
		return objects.size();
	}
	
	/**
	 * Returns the number of objects waiting to be synchronized with the server.
	 * @return The number of objects to be synchronized
	 */
	public int queueSize() {
		return toBeCreated.size() + toBeDeleted.size() + toBeUpdated.size();
	}
	
	/**
	 * Adds a listener to this object
	 * @param listener Object that will listen.
	 */
	public void addPropertyChangeListener(PropertyChangeListener listener) {
		changeSupport.addPropertyChangeListener(listener);
	}

	/**
	 * Removes a listener from this object
	 * @param listener Object that will stop listening.
	 */
	public void removePropertyChangeListener(PropertyChangeListener listener) {
		changeSupport.removePropertyChangeListener(listener);
	}

	public ArrayList<E> getCopyOfList() {
		synchronized(objects) {
			return new ArrayList<E>(objects);
		}
	}
	
	/**
	 * Receives updates from the elements contained in this list.
	 * If they should be edited an edit window is displayed.
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		if(evt.getPropertyName().equals("edit")) {
			if(evt.getSource() instanceof Meeting) {
				MeetingOverview over = new MeetingOverview((Meeting)evt.getSource());
				over.setVisible(true);
			}
			else if(evt.getSource() instanceof Appointment) {
				AppointmentOverview over = new AppointmentOverview((Appointment)evt.getSource());
				over.setVisible(true);
			}
			else if(evt.getSource() instanceof Invite){
				InviteOverview over = new InviteOverview();
				over.setVisible(true);
			}
		} else {
			synchronized(toBeUpdated){
				toBeUpdated.add((E)evt.getSource());
			}
			synchronize();
		}
		
		Storage.updateUI();
	}
	
	@Override
	public Iterator<E> iterator() {
		return objects.iterator();
	}
	
	/**
	 * Sorts the list.
	 */
	public void sort() {
		synchronized(objects) {
			if(objects.size() > 0)
				Collections.sort(objects);
		}
	}
	
	/**
	 * Clear all data from the list
	 */
	public void clearList() {
		objects.clear();
		toBeCreated.clear();
		toBeUpdated.clear();
		toBeDeleted.clear();
	}
	
	@Override
	public String toString() {
		String a  = "";
		for(E e : objects) {
			a += e+"\n";
		}
		return a;
	}
}
